Add Node.js / Bun bindings via napi-rs - #15
Merged
Conversation
Add a napi-rs binding crate exposing the DocumentConverter to Node.js and Bun via a single native N-API addon (Bun implements N-API, so the same binary loads in both — no rebuild). The TypeScript surface mirrors the Rust API: - convertFile / convert (in-memory bytes) plus Promise-returning *Async variants that run the CPU-bound work off the event loop - Markdown or docling-core JSON output; strict Markdown; placeholder / embedded / referenced image modes; allowedFormats; fetchImages - a reusable DocumentConverter class - streamFileMarkdown: an async generator over Markdown chunks in document order (the streaming win for PDF), wrapping the native callback API - supportedFormats / formatFromName helpers Packaging: @napi-rs/cli build, generated + hand-written TypeScript types, a self-referencing exports map (works from ESM and CommonJS), a smoke test that passes identically under Node and Bun, and runnable Node + Bun/TS examples. Generated artifacts (.node, native.js/.d.ts, node_modules) are gitignored; run `npm install && npm run build`. The crate joins the workspace (covered by fmt/clippy/test) but is marked publish=false — it ships to npm, not crates.io. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01L7XwjsWSHWjrh39JxMLuB3
Add a reusable `Pipeline` class that keeps the ONNX models (layout, OCR,
TableFormer with the KV-cached decoder) loaded across calls — the one-shot
convert functions rebuild the pipeline and reload every model each call.
Handles pdf/image inputs; synchronous, reuse one instance across documents.
Add `installDependencies()` / `checkDependencies()` for the PDF/image assets
that aren't bundled in the addon, mirroring how docling provisions models:
- pdfium (bblanchon, platform-detected) and the PP-OCR model + dictionary are
downloaded automatically over HTTP(S);
- the layout and TableFormer ONNX (PyTorch->ONNX exports with no public
prebuilt download) are fetched from a base URL supplied via
`{ modelsUrl }` or `FLEISCHWOLF_MODELS_URL`, or detected from local
`DOCLING_*` env paths;
- everything installs under `~/.cache/fleischwolf` (or `$FLEISCHWOLF_HOME` /
`dir`) and the matching `DOCLING_*` / `PDFIUM_DYNAMIC_LIB_PATH` env vars are
wired in-process. Idempotent; throws with an actionable message when PDF
conversion can't be made ready.
Guard every entry point (module functions + DocumentConverter + Pipeline +
streamFileMarkdown): converting a pdf/image/METS input before the deps are
installed now throws (or rejects, for async) an error pointing at
`installDependencies()`. Declarative formats are unaffected.
Docs: package + root README document the model provisioning and warm Pipeline;
add examples/pdf-pipeline.mjs. Smoke test covers the guards and dependency
status (16 checks, passing under Node and Bun).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01L7XwjsWSHWjrh39JxMLuB3
…lease Add .github/workflows/npm-publish.yml: on each `v*` release tag (pushed by scripts/release.sh), build the native addon on a matrix of native runners — Linux x64 (ubuntu-22.04), Linux arm64 (ubuntu-24.04-arm), macOS x64 (macos-13), macOS arm64 (macos-14), Windows x64 — then a publish job assembles the main `fleischwolf` package plus per-platform `fleischwolf-<triple>` packages (optionalDependencies) via napi-rs and publishes them all to npm. The version is taken from the tag so it tracks the crate release; already-published versions are skipped for idempotent re-runs. Configure the package for the napi-rs v2 multi-platform flow: napi.name + napi.triples for the five targets, and prepublishOnly / artifacts / version scripts. The main package ships only the JS loader + types (~50 kB); the prebuilt .node binaries ride in the platform packages. README documents `npm install fleischwolf` (prebuilt) alongside build-from-source. Requires an NPM_TOKEN repository secret with publish rights. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01L7XwjsWSHWjrh39JxMLuB3
Switch npm-publish.yml from an automatic `v*` tag trigger to workflow_dispatch-only: pick the release tag to build, and the workflow checks out that tag (all matrix + publish jobs) and publishes the npm version derived from it. Keeps npm releases decoupled from the crates.io release that runs on each master push. An optional `version` input overrides the derived version. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01L7XwjsWSHWjrh39JxMLuB3
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Adds native TypeScript bindings for Fleischwolf that run in both Node.js and Bun via N-API. The bindings expose the full converter API with feature parity to the Rust library.
Summary
This introduces
crates/fleischwolf-node, a new napi-rs crate that compiles to a native.nodeaddon. The addon loads in both Node.js and Bun (which implements N-API) without requiring a rebuild between runtimes. The TypeScript surface mirrors the RustDocumentConverter, supporting:convert,convertFile,convertAsync,convertFileAsync)streamFileMarkdownasync generator (chunks in document order)DocumentConverter) for batch processingKey Changes
crates/fleischwolf-node/src/lib.rs(627 lines): Core N-API binding implementationconvert_file,convert,convert_file_async,convert_async)DocumentConverterclass with sync/async methods and streaming supportConvertConfigandRawResulttypes for thread-safe off-loop executionconvert_file_streamingwith threadsafe callbackscrates/fleischwolf-node/index.js: Public entry pointstreamFileMarkdownas an async generator wrapping the native callback-based streaming APIcrates/fleischwolf-node/index.d.ts: TypeScript type definitions for the public APIcrates/fleischwolf-node/package.json: npm package metadatacrates/fleischwolf-node/Cargo.toml: Rust crate configurationcdyliblibrary type for N-API addonfleischwolf,napi,napi-derive,napi-buildcrates/fleischwolf-node/build.rs: Build script for N-API symbol resolutionDocumentation and examples:
README.md: Comprehensive guide with API reference, quick start, and examplesexamples/node-basic.mjs: Node.js ESM example (file conversion, bytes, JSON, reusable converter)examples/bun-basic.ts: Bun TypeScript example (async, streaming, embedded images)test/smoke.mjs: Smoke test covering all major code pathsRoot
Cargo.toml: Addedcrates/fleischwolf-nodeto workspace membersNotable Implementation Details
ConvertConfigandRawResultare Send-safe (no napi types) to cross thread boundariesThreadsafeFunctionwithErrorStrategy::CalleeHandledto marshal chunks from a background thread back to the JS event loop"pdf","md") and extensions (".html",".docx")Errorwith appropriate status codesstreamFileMarkdownbridges the native callback-based API into a clean async-iterable interfacehttps://claude.ai/code/session_01L7XwjsWSHWjrh39JxMLuB3